<?php
###########################################################################
#  Copyright (C) 2006-2007 Glyphix, Inc. <briz@glyphix.com>               #
#                                                                         #
#  Permission is hereby granted, free of charge, to any person obtaining  #
#  a copy of this software and associated documentation files (the        #
#  "Software"), to deal in the Software without restriction, including    #
#  without limitation the rights to use, copy, modify, merge, publish,    #
#  distribute, sublicense, and/or sell copies of the Software, and to     #
#  permit persons to whom the Software is furnished to do so, subject to  #
#  the following conditions:                                              #
#                                                                         #
#  The above copyright notice and this permission notice shall be         #
#  included in all copies or substantial portions of the Software.        #
#                                                                         #
#  Except as contained in this notice, the name(s) of the above           #
#  copyright holders shall not be used in advertising or otherwise to     #
#  promote the sale, use or other dealings in this Software without       #
#  prior written authorization.                                           #
#                                                                         #
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        #
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     #
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. #
#  IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR      #
#  OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,  #
#  ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR  #
#  OTHER DEALINGS IN THE SOFTWARE.                                        #
###########################################################################

/**
 * GXGoogleDocs
 *
 * Functions to work with Google Docs.
 * @author		Brad Brizendine <briz@glyphix.com>
 * @link		http://www.glyphix.com/
 * @license		http://opensource.org/licenses/bsd-license.php BSD License
 * @version		1.0
 * @package		GXPage
 * @subpackage	Modules
 * @copyright	Copyright 2006 - 2007, Glyphix, Inc.
 * @uses		GXModule, curl
 */

class GXGoogleDocs extends GXGoogleAuth {

	// curl user agent string
	private $userAgent = 'Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.4) Gecko/20030624 Netscape/7.1 (ax)';

	// cache curl connection
	private $curl;

	// cache list of docs
	private $list;
	// cache docs
	private $cache;
	// database name, key, worksheet
	private $dbname;
	private $key;
	private $worksheet;

	// init GXGoogleAuth
	public function __construct( $config = null ){
		parent::__construct($config);
		// set the service to the Google Docs service id
		$this->service = 'wise';
		// store the gxcurl object
		$this->curl = new GXCurl();
		// set the name, key and worksheet
		if( $dbname = $this->getval($config, 'database') ){
			$this->dbname = $dbname;
		}
		if( $key = $this->getval($config, 'key') ){
			$this->key = $key;
		}
		if( $worksheet = $this->getval($config, 'worksheet') ){
			$this->worksheet = $worksheet;
		}
		return true;
	}

	public function getDocName(){
		return $this->dbname;
	}

	private function gheader(){
		$header = array(
			'Authorization: GoogleLogin auth=' .$this->getToken(),
			'Content-Type: application/atom+xml'
			);
		return $header;
	}

	private function getStreamContext(){
		return stream_context_create(array(
			'http' => array(
				'method' => 'GET',
				'header' => implode("\r\n", $this->gheader())
			)
		));
	}

	public function listSpreadsheets(){
		// if we're not logged in, try it
		if( !$this->getToken() ){
			// attempt a client login
			if( !$this->login() ){
				trigger_error( 'Missing token ... call login() first', E_USER_ERROR );
				return false;
			}
		}

		// return the cached one if we have it
		if( $this->list ){
			return $this->list;
		}

		/*
		// use php streams instead of curl, since they're faster
		$streamcontext = array(
			'http' => array(
				'method' => 'GET',
				'header' => implode("\r\n", $this->gheader())
			)
		);
		$this->list = new GXDOM(file_get_contents('http://spreadsheets.google.com/feeds/spreadsheets/private/full', false, $this->getStreamContext()));
		*/

		// get the spreadsheet list url
		$this->curl->setHeader( $this->gheader() );
		$this->list = new GXDOM( $this->curl->fetch('http://spreadsheets.google.com/feeds/spreadsheets/private/full') );
		trigger_error('------------ ' .$this->list->exec->xpath('/*', 0)->exec->xml(), E_USER_ERROR);
		if( !$this->list ){
			trigger_error( 'Unable to get the list of spreadsheets', E_USER_ERROR );
			return false;
		}
		return $this->list;
	}

	public function getSpreadsheet( $name = null, $key = null ){
		// if we have a key, bypass the whole listing spreadsheet thing
		if(!$key) $key = $this->key;
		if($key) return $this->getSpreadsheetByKey($key);

		// if we have a name, we have to list the spreadsheets
		if(!$name) $name = $this->dbname;
		if(!$name){
			trigger_error('No database name provided and none in the config.', E_USER_ERROR);
			return false;
		}

		// get the list
		$list = $this->listSpreadsheets();
		if( !$list ){
			trigger_error( 'Unable to get list of spreadsheets, cannot continue.', E_USER_ERROR );
			return false;
		}

		trigger_error('--------------------- ' .$list->exec->xml(), E_USER_ERROR);

		// look for the spreadsheet with the named title
		$sheet = $list->exec->xpath( '//atom:entry[ atom:title = "' .$name .'" ]', array('atom','http://www.w3.org/2005/Atom'), 0 );
		if( !$sheet ){
			trigger_error( 'No spreadsheet by that name (' .$name .')', E_USER_ERROR );
			return false;
		}

		// cache the spreadsheet
		$this->cache[ $name ] = new GXGoogleSpreadsheet( $this->curl, $this->getToken(), $sheet );
		return $this->cache[ $name ];
	}

	public function getSpreadsheetByKey($key = null, $worksheet = null){
		if(!$key){
			trigger_error('Missing key (' .$key .')', E_USER_ERROR);
			return false;
		}
		// return the cached one if we have it
		if( $this->cache[$key] ){
			return $this->cache[$key];
		}

		// if no worksheet value, get the first one
		if(!$worksheet) $worksheet = 1;

		// if we're not logged in, try it
		if( !$this->getToken() ){
			// attempt a client login
			if( !$this->login() ){
				trigger_error( 'Missing token ... call login() first', E_USER_ERROR );
				return false;
			}
		}

		// get the spreadsheet
		$this->curl->setHeader( $this->gheader() );
		$data = $this->curl->fetch('http://spreadsheets.google.com/feeds/list/' .$key .'/' .$worksheet .'/private/full');
		if(!$data){
			trigger_error('Unable to get spreadsheet by key (' .$key .')', E_USER_ERROR);
			return false;
		}

		// add a feed for this spreadsheet
		$sheet = new GXGoogleSpreadsheet($this->curl, $this->getToken(), new GXDOM($data), $key, $worksheet);
		$this->cache[$key] = $sheet;
		trigger_error('Retrieved spreadsheet data for key ' .$key, E_USER_NOTICE);
		return $this->cache[$key];
	}

}

class GXGoogleSpreadsheet {

	private $curl;					// class var to cache the curl connection
	private $token;					// the authentication token after calling Google's ClientLogin
	private $sheet;					// the atom xml descriptor for the sheet, not the data itself
	private $name;					// the textual name of this spreadsheet
	private $key;					// the unique id of the spreadsheet
	private $feeds;					// associative array of feed names, urls and data
	/*
	private $worksheetsurl = false;	// url to get list of  worksheets in this spreadsheet (false if none found)
	private $worksheets = false;	// worksheets atom xml descriptor for the worksheets in this document
	*/
	private $writeable = true;		// boolean for this user's write privilege
	private $feedurl;				// core uri for the main feed ... I think
	private $posturl;				// the url to use for changes to the document

	// log events
	private $log;

	public function __construct( $curl = null, $token = null, $sheet = null, $key = null, $worksheet = null ){
		$this->curl = $curl;
		$this->token = $token;
		$this->sheet = $sheet;
		$this->key = $key;
		$this->name = $key ? $key : $this->sheet->exec->xpath( 'atom:title', array('atom','http://www.w3.org/2005/Atom'), 0 )->nodeValue;

		$this->log = new GXDOM('Log');
		$this->log->token($token);
		$this->log->sheet($sheet);
		$this->log->name($this->name);

		// if we have a key and $sheet, brute force add its feed
		if($key && $sheet){
			// set up empty stub for the worksheets feed
			$this->feeds['worksheets'] = array(
				'url'	=> 'https://spreadsheets.google.com/feeds/worksheets/' .$key .'/private/full',
				'data'	=> true
			);
			$this->addFeed($key, 'http://spreadsheets.google.com/feeds/list/' .$key .'/' .$worksheet .'/private/full', $sheet);
		}else{
			// determine if this user has write privileges
			$link = $this->sheet->exec->xpath( 'atom:link[ contains(@rel,"worksheetsfeed") ]/@href', array('atom','http://www.w3.org/2005/Atom'), 0 );
			if( !$link ){
				trigger_error( 'No worksheetsfeed in this document (' .$this->name .'), cannot determine write privileges', E_USER_ERROR );
				$this->writeable = false;
				$this->log->write('Cannot find a worksheets feed for this document');
			}else{
				// store the worksheetsfeed in this object
				$this->addFeed( 'worksheets', $link->nodeValue );
				// determine if the user has write privileges
				$parts = split('/',$link->nodeValue);
				$full = array_pop($parts);
				$private = array_pop($parts);
				if( $full != 'full' || $private != 'private' ){
					trigger_error( 'This user does not have access to this spreadsheet (' .$this->name .'); cannot continue', E_USER_ERROR );
					$this->writeable = false;
					$this->log->write('This user does not have write privileges on this document (' .$this->name .')');
				}
			}
		}

		$this->log->init('Complete');
		return true;
	}

	public function getLog(){
		$this->log->writeable( $this->writeable );
		$this->log->feeds( $this->feeds );
		$this->log->feedurl( $this->feedurl );
		$this->log->posturl( $this->posturl );
		$this->log->feeds( $this->feeds );
		return $this->log;
	}

	public function addFeed( $name = null, $url = null, $data = null ){
		if( !$name ){
			trigger_error( 'No name supplied to addFeed', E_USER_ERROR );
			return false;
		}
		// if it's not cached already, build the whole thing
		if( !isset($this->feeds[$name]) ){
			$this->feeds[ $name ] = array(
				'url'	=> $url,
				'data'	=> $data
				);
			return true;
		}
		// if it's already there, overwrite only if we have a value
		if( $url ){
			$this->feeds[ $name ]['url'] = $url;
		}
		if( $data ){
			$this->feeds[ $name ]['data'] = $data;
		}
		return true;
	}

	private function gheader(){
		$header = array(
			'Authorization: GoogleLogin auth=' .$this->token,
			'Content-Type: application/atom+xml'
			);
		return $header;
	}

	public function getWorksheets(){
		trigger_error('--------------- getWorksheets ', E_USER_ERROR);
		$ws = new GXDOM( $this->curl->fetch($this->feeds['worksheets']['url']) );
		if( !$ws ){
			$msg = 'Unable to get the worksheet feed';
			trigger_error( $msg, E_USER_ERROR );
			$this->log->ERROR($msg);
		}
		$this->addFeed( 'worksheets', $this->feeds['worksheets']['url'], $ws );

		// get the feedurl for the first worksheet
		$link = $ws->exec->xpath( '/atom:feed/atom:entry[1]/atom:link[ contains(@rel,"#listfeed") ]/@href', array('atom','http://www.w3.org/2005/Atom'), 0 );
		if( !$link ){
			$msg = 'Unable to find the feedurl for this spreadsheet (' .$this->name .')';
			trigger_error( $msg, E_USER_ERROR );
			$this->log->ERROR($msg);
			return false;
		}
		$this->feedurl = $link->nodeValue;
		// dupe into posturl ... should do a search on #post, but it ain't working
		$this->posturl = $link->nodeValue;

		/*
		// get the posturl for the first worksheet
		$link = $ws->exec->xpath( '/atom:feed/atom:link[ contains(@rel,"#post") ]/@href', array('atom','http://www.w3.org/2005/Atom'), 0 );
		if( !$link ){
			$msg = 'Unable to find the posturl for this spreadsheet (' .$this->name .')';
			trigger_error( $msg, E_USER_ERROR );
			$this->log->ERROR($msg);
			return false;
		}
		$this->posturl = $link->nodeValue;
		*/

		// we have the worksheets now, so we're done
		return true;
	}

	public function getListData(){
		trigger_error('-------------- getListData():', E_USER_ERROR);
		if( ! is_object($this->feeds['worksheets']['data']) ){
			$this->getWorksheets();
		}

		// if it's already there, just return it
		$foo = $this->key ? $this->key : $this->name;
		if($this->feeds[$foo]['data']){
			return $this->feeds[$foo]['data'];
		}

		// get the listfeed url for the first worksheet
		$listfeedurl = $this->feeds['worksheets']['data']->exec->xpath( '//atom:entry[1]/atom:link[ contains(@rel,"#listfeed") ]/@href', array('atom','http://www.w3.org/2005/Atom'), 0 );
		if( !$listfeedurl ){
			$msg = 'Unable to locate the listfeed url for the first worksheet in this spreadsheet (' .$this->name .')';
			trigger_error($msg);
			$this->log->ERROR($msg);
			return false;
		}

		// get the data
		return new GXDOM( $this->curl->fetch($listfeedurl->nodeValue) );
	}

	public function set( $data = null ){
		if( !$data ){
			$msg = 'No data to set';
			trigger_error( $msg, E_USER_ERROR );
			$this->log->ERROR($msg);
			return false;
		}

		// make sure we've called worksheets, or we won't have a posturl
		if( ! is_object($this->feeds['worksheets']['data']) ){
			$this->getWorksheets();
		}

		$this->log->header( $this->gheader() );
		$this->log->data( $data );
		// set curl's header
		$this->curl->setHeader( $this->gheader() );
		// store the new row
		$url = $this->key ? $this->feeds[$this->key]['url'] : $this->posturl;
		$r = $this->curl->fetch( $url, $data, GXCurl::POST );
		if( !$r ){
			$msg = 'Unable to create new record';
			trigger_error( $msg, E_USER_ERROR );
			$this->log->ERROR($msg);
			return false;
		}
		return $r;
	}

	public function add($data = null){
		if(!$data){
			$msg = 'No data to create the row';
			trigger_error($msg, E_USER_ERROR);
			$this->log->ERROR($msg);
			return false;
		}

		// make sure we've called worksheets, or we won't have a posturl
		if( ! is_object($this->feeds['worksheets']['data']) ){
			$this->getWorksheets();
		}

		// send data to the api
		$this->log->header($this->gheader());
		$this->log->data($data);
		// set curl's header
		$header = $this->gheader();
		$this->curl->setHeader($header);
		// add the row
		$url = $this->key ? $this->feeds[$this->key]['url'] : $this->posturl;
		$r = $this->curl->fetch( $url, $data, GXCurl::POST );
		if( !$r ){
			$msg = 'Unable to add the record (' .print_r($data, true) .')';
			trigger_error($msg, E_USER_ERROR);
			$this->log->payload($r);
			$this->log->ERROR($msg);
			return false;
		}
		return $r;
	}

	public function delete($url = null){
		if(!$url){
			$msg = 'No url to delete';
			trigger_error($msg, E_USER_ERROR);
			$this->log->ERROR($msg);
			return false;
		}

		// set curl's header
		$head = $this->gheader();
		$this->log->header($head);
		$this->log->data($url);
		// delete even if there are newer versions of this row
		$head['If-Match'] = '*';
		$this->curl->setHeader($head);
		// delete the row
		$r = $this->curl->fetch($url, GXCurl::DELETE);
		trigger_error('Result of curl DELETE command: ' .var_export($r, true), E_USER_NOTICE);
		if( !$r ){
			$msg = 'Unable to delete the record (' .$url .')';
			trigger_error($msg, E_USER_ERROR);
			$this->log->ERROR($msg);
			return false;
		}
		trigger_error('=== Deleted row (' .$url .')', E_USER_WARNING);
		return $r;
	}

	public function update($url = null, $data = null){
		if( ! $url || ! $data){
			$msg = 'No url and/or data to update the row';
			trigger_error($msg, E_USER_ERROR);
			$this->log->ERROR($msg);
			return false;
		}

		// make sure we've called worksheets, or we won't have a posturl
		if( ! is_object($this->feeds['worksheets']['data']) ){
			$this->getWorksheets();
		}

		// send data to the api
		$this->log->header($this->gheader());
		$this->log->data($data);
		// set curl's header
		$header = $this->gheader();
		$this->curl->setHeader($header);
		$r = $this->curl->fetch( $url, $data, GXCurl::PUT );
		if( !$r ){
			$msg = 'Unable to update the record (' .print_r($data->exec->xml(), true) .')';
			trigger_error($msg, E_USER_ERROR);
			$this->log->payload($r);
			$this->log->ERROR($msg);
			return false;
		}
		return $r;
	}

	// clear the spreadsheet ... can't do it by row, because that just decrements the number of rows in the sheet
	// set the number of rows to 2...short cut to deleting each row one by one
	public function clearSheet(){
		$dbxml = $this->getListData();
		// pull the delete urls from each entry
		$urls = $dbxml->exec->xpath('//atom:entry/atom:link[@rel="edit"]/@href', array('atom'=>'http://www.w3.org/2005/Atom', 'gsx'=>'http://schemas.google.com/spreadsheets/2006/extended'));
		// issue the delete command for each
		foreach($urls as $entry){
			$rowurl = $entry->nodeValue;
			$this->delete($rowurl);
		}
		return true;
	}

	// shortcut to deleting each row one by one ... sets the number of rows to the supplied number, and will delete without warning
	public function truncateSheetTo($rows = null){
		trigger_error('-------------- truncateSheetTo(' .$rows .'):', E_USER_ERROR);
		if( ! is_object($this->feeds['worksheets']['data']) ){
			$this->getWorksheets();
		}

		// store the gxdom object
		$wsinfo = $this->feeds['worksheets']['data'];

		// grab the ws entry
		$ws = $wsinfo->exec->xpath('//atom:entry[1]', 0, array('atom'=>'http://www.w3.org/2005/Atom', 'gsx'=>'http://schemas.google.com/spreadsheets/2006/extended'));

		// grab the rowCount element and update its value ... it's still tied to the $ws object
		$rowCount = $ws->exec->xpath('//gs:rowCount', 0, array('atom'=>'http://www.w3.org/2005/Atom', 'gsx'=>'http://schemas.google.com/spreadsheets/2006/extended', 'gs'=>'http://schemas.google.com/spreadsheets/2006'));
		$rowCount->nodeValue = $rows;

		// grab the worksheet edit url
		$editurl = $ws->exec->xpath('//atom:link[@rel="edit"]/@href', 0, array('atom'=>'http://www.w3.org/2005/Atom', 'gsx'=>'http://schemas.google.com/spreadsheets/2006/extended'))->nodeValue;

		// now update the rowsCount
		$head = $this->gheader();
		$this->log->header($head);
		$this->log->data($editurl);
		$this->curl->setHeader($head);
		$r = $this->curl->fetch($editurl, $ws, GXCurl::PUT);

		// all done
		return true;
	}

}

?>
